package com.oneandseven.dajia.base.generator.snowflake;


import lombok.extern.slf4j.Slf4j;

import java.io.*;
import java.nio.channels.FileLock;
import java.util.*;

/**
 * 雪花算法WorkerId生成器，防止在同一台机器上部署多个微服务时，
 * 因workerId一样导致生成的主键ID重复。
 * <p>
 * 多个微服务部署在同一个节点上时，雪花算法在获取服务对应的workerID时：各节点通过竞争文件锁，然后依次递增设置自己对应的workerId值，保存到文件中。文件类型为property文件，
 * <p>
 * 存储值格式为：微服务所在路径=当前时间毫秒数@workerID， 其中workerID取值范围为：0-31
 *
 * @author LP
 * @date 2021/12/14 7:49
 */
@Slf4j
public class SnowFlakeWorkIdGenerator {

    public static final Long IN_VALID_WORKER_ID = 999L;

    public static void main(String[] args) {
        long id = generateWorkerId();
        log.info("获取到的workId为：{}", id);
    }

    /**
     * 生成雪花算法
     *
     * @return
     */
    public static Long generateWorkerId() {
        //文件锁对应的文件
        String userHome = System.getProperty("user.home");
        File snowFlakeDir = new File(userHome);
        File lockFile = new File(snowFlakeDir, "workerProperty.lock");
        //workerId起始值
        long workerId = 0;
        try(RandomAccessFile raf = new RandomAccessFile(lockFile ,"rw")){
            log.info("雪花算法---准备获取workerId生成文件锁");
            FileLock fileLock = raf.getChannel().lock();
            File proFile = new File(snowFlakeDir, "workerProperty.properties");
            if (!proFile.exists()) {
                try {
                    proFile.createNewFile();
                } catch (IOException e) {
                    log.error("创建用来存储雪花算法中WorkerId的文件报错，请确定在目录：" + snowFlakeDir.getAbsolutePath() + " 下是否有文件读写权限，系统将因此退出", e);
                    System.exit(1);
                }
            }
            log.info("雪花算法---成功获取到workerId生成文件锁");
            try (FileInputStream inputStream = new FileInputStream(proFile)) {
                Properties properties = new Properties();
                properties.load(inputStream);
                //加载workerID属性文件中已有的所有workerId的定义
                Set<String> propList = properties.stringPropertyNames();
                //获取当前应用路径
                String appPath = getAppPath();
                if (!propList.contains(appPath)) {
                    long prefixNum = getBiggestWorkerSeq(properties, propList);
                    workerId = prefixNum + 1;
                    if (workerId > 31) {
                        //如果当前分配的workerId已经达到最大值,则清理那些不存在的应用路径
                        TreeMap<String, Long> invalidedList = findInvalidedPaths(properties, propList);
                        //检测是否有过期的路径，如果有，则复用，否则，判断当前workerID是否已经全部分配完成，如果已经全部分配完成，则抛出异常，否则找到中间缺失的值进行分配设置
                        if (invalidedList.isEmpty()) {
                            if (propList.size() == 32) {
                                //如果所有路径均有效，0-31个workerID已经全部分配完成
                                log.error("当前机器部署的微服务数据量已经达到极限,主键生成机制不可用，系统将退出");
                                System.exit(1);
                            } else {
                                //找出缺失的0-30中间的数字
                                long missedSeq = findMissedSeq(properties, propList);
                                if (missedSeq == IN_VALID_WORKER_ID) {
                                    log.error("当前机器部署的微服务数据量已经达到极限,主键生成机制不可用，系统将退出");
                                    System.exit(1);
                                }
                                workerId = missedSeq;
                            }
                        } else {
                            String key = invalidedList.firstEntry().getKey();
                            Long seq = invalidedList.firstEntry().getValue();
                            properties.remove(key);
                            workerId = seq;
                        }
                    }
                    properties.put(appPath, System.currentTimeMillis() + "@" + workerId);
                    try (FileOutputStream outputStream = new FileOutputStream(proFile)) {
                        properties.store(outputStream, "更新workerId" + System.currentTimeMillis());
                    }
                } else {
                    workerId = findExistedProperty(properties, appPath);
                }
            } finally {
                fileLock.release();
            }
        } catch (Exception e) {
            log.error("生成雪花算法的所需的WorkerId报错,主键生成机制不可用，系统将退出", e);
            System.exit(1);
        }
        return workerId;
    }

    /**
     * 获取可以唯一标识当前应用的路径，当前直接通过当前类的classpath路径获取
     *
     * @return 唯一标识当前应用的路径
     */
    private static String getAppPath() {
        String curPath = SnowFlakeWorkIdGenerator.class.getProtectionDomain().getCodeSource().getLocation().getPath();
        if (curPath.contains("!/BOOT-INF/") && curPath.startsWith("file:/")) {
            String springBootJarPath = curPath.substring("file:/".length(), curPath.indexOf("!/BOOT-INF/"));
            if (isLinuxOS()) {
                springBootJarPath = "/" + springBootJarPath;
            }
            log.info("当前微服务所在路径为：{}", springBootJarPath);
            return springBootJarPath;
        }
        return curPath;
    }

    private static boolean isLinuxOS() {
        boolean flag = false;
        String osType = System.getProperty("os.name");
        if (osType.toLowerCase().contains("linux")) {
            flag = true;
        }
        return flag;
    }

    /**
     * 获取最大的workerId
     *
     * @param properties 存储workerID的属性文件对象
     * @param propList   每个服务的路径列表
     * @return 最大的workerId
     */
    private static long getBiggestWorkerSeq(Properties properties, Set<String> propList) {
        List<Long> workerIdList = new ArrayList<>();
        for (String propKey : propList) {
            String value = (String) properties.get(propKey);
            String[] values = value.split("@");
            workerIdList.add(Long.valueOf(values[1]));
        }
        if (workerIdList.isEmpty()) {
            return -1L;
        }
        Collections.sort(workerIdList);
        return workerIdList.get(workerIdList.size() - 1);
    }

    /**
     * 获取已经不存在的的应用程序路径和其对应的workerId
     *
     * @param properties 存储workerID的属性文件对象
     * @param propList   每个服务的路径列表
     * @return 已经不存在的的应用程序路径和其对应的workerId
     */
    private static TreeMap<String/*path*/, Long/*seq*/> findInvalidedPaths(Properties properties, Set<String> propList) {
        TreeMap<String/*服务路径*/, Long/*对应的workerId*/> sortedWorker = new TreeMap<>();
        for (String propKey : propList) {
            File filePath = new File(propKey);
            if (!filePath.exists()) {
                String value = (String) properties.get(propKey);
                String[] values = value.split("@");
                sortedWorker.put(propKey, Long.valueOf(values[1]));
            }
        }
        return sortedWorker;
    }

    /**
     * 找到0-30中间缺失的数字，之前分配过的workerId,因为服务部署换了路径，原有路径不存在了，
     * 程序在生成workerId时进行了清理，导致原本依次递增的数字中间出现了缺失。如：
     * path1=xxxx@1
     * path3=xxxx@3
     * path31=xxxx@31
     * <p>
     * 则除了 1、3之外的数字都是因路径不存而丢失的数字
     *
     * @param properties 存储workerID的属性文件对象
     * @param propList   所有服务的路径列表
     * @return 随机找出一个在文件内容中缺失的0-31之间的一个数值
     */
    private static Long findMissedSeq(Properties properties, Set<String> propList) {
        TreeMap<String, Long> sortedWorker = new TreeMap<>();
        for (int i = 0; i < 32; i++) {
            sortedWorker.put(String.valueOf(i), (long) i);
        }
        for (String propKey : propList) {
            String value = (String) properties.get(propKey);
            String[] values = value.split("@");
            long seq = Long.parseLong(values[1]);
            String key = String.valueOf(seq);
            if (sortedWorker.containsKey(key)) {
                sortedWorker.put(key, 999L);
            }
        }
        return sortedWorker.values().stream().filter(seq -> seq != 999 && seq != 31).findFirst().orElse(999L);
    }


    /**
     * 根据当前服务对应的路径获取分配给他的workerId值
     *
     * @param properties 存储workerID的属性文件对象
     * @param appPath    当前服务的路径
     * @return 当前服务被分配的workerId
     */
    private static Long findExistedProperty(Properties properties, String appPath) {
        String propValue = properties.getProperty(appPath);
        String[] values = propValue.split("@");
        return Long.valueOf(values[1]);
    }
}

